#ifndef MATRIX_HPP
#define MATRIX_HPP

#include <iostream>
#include <iomanip>
#include <cmath>

#include <boost/array.hpp>

#include <QtAlgorithms>

#include "Math.hpp"

#include "Vector2.hpp"
#include "Vector3.hpp"
#include "Vector4.hpp"

namespace Neon
{
/*!
* Defines a matrix.
*/
class NEON_DLL Matrix
{
public:
	/*!
	Create a Matrix.
	*\param isIdentity true if Identity; false otherwise.
	*/
	explicit Matrix(bool isIdentity = true);

	/*!
	Creates a custom Matrix.
	*\param m11 Value to initialise row 1 column 1.
	*\param m12 Value to initialise row 1 column 2.
	*\param m13 Value to initialise row 1 column 3.
	*\param m14 Value to initialise row 1 column 4.
	*\param m21 Value to initialise row 2 column 1.
	*\param m22 Value to initialise row 2 column 2.
	*\param m23 Value to initialise row 2 column 3.
	*\param m24 Value to initialise row 2 column 4.
	*\param m31 Value to initialise row 3 column 1.
	*\param m32 Value to initialise row 3 column 2.
	*\param m33 Value to initialise row 3 column 3.
	*\param m34 Value to initialise row 3 column 4.
	*\param m41 Value to initialise row 4 column 1.
	*\param m42 Value to initialise row 4 column 2.
	*\param m43 Value to initialise row 4 column 3.
	*\param m44 Value to initialise row 4 column 4.
	*/
	explicit Matrix(const nfloat m11, const nfloat m12, const nfloat m13, const nfloat m14,
					const nfloat m21, const nfloat m22, const nfloat m23, const nfloat m24,
					const nfloat m31, const nfloat m32, const nfloat m33, const nfloat m34,
					const nfloat m41, const nfloat m42, const nfloat m43, const nfloat m44);

	/*!
	Creates a Matrix from another Matrix.
	*\param mat Source matrix.
	*/
	Matrix(const Matrix &mat);

	/*!
	Creates a Matrix from a scalar by assigning the scalar.
	*\param mat Matrix scalar.
	*/
	Matrix(const nfloat mat);

	/*!
	Creates a Matrix from a array nfloat Matrix.
	*\param mat Source array matrix.
	*/
	Matrix(const nfloat *mat);

	/*!
	Creates a Matrix from another Matrix.
	*\param rhs Source matrix.
	*\return The copied matrix.
	*/
	Matrix& operator =(const Matrix &rhs);

	/*!
	Creates a Matrix from a scalar by assigning the scalar.
	*\param rhs Matrix scalar.
	*\return The copied scalar.
	*/
	Matrix& operator =(const nfloat rhs);

	/*!
	Creates a Matrix from a array nfloat Matrix.
	*\param rhs Source array matrix.
	*\return The copied matrix.
	*/
	Matrix& operator =(const nfloat *rhs);

	/*!
	Inverts negative values to postive.
	*\return A postive matrix.
	*/
	Matrix operator +() const;

	/*!
	Adds two Matrix.
	*\param rhs Source matrix.
	*\return Sum of two matrices.
	*/
	Matrix operator +(const Matrix &rhs) const;

	/*!
	Adds a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Sum of the matrix and scalar.
	*/
	Matrix operator +(nfloat rhs) const;

	/*!
	Adds a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Sum of the matrix and source array matrix.
	*/
	Matrix operator +(const nfloat *rhs) const;

	/*!
	Adds two Matrix.
	*\param rhs Source matrix.
	*\return Sum of two matrices.
	*/
	Matrix& operator +=(const Matrix &rhs);

	/*!
	Adds a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Sum of the matrix and scalar.
	*/
	Matrix& operator +=(nfloat rhs);

	/*!
	Adds a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Sum of the matrix and source array matrix.
	*/
	Matrix& operator +=(const nfloat *rhs);

	/*!
	Inverts postive values to negative.
	*\return A negative matrix.
	*/
	Matrix operator -() const;

	/*!
	Subtracts two Matrix.
	*\param rhs Source matrix.
	*\return Result of the subtraction.
	*/
	Matrix operator -(const Matrix &rhs) const;

	/*!
	Subtracts a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Result of the subtraction.
	*/
	Matrix operator -(nfloat rhs) const;

	/*!
	Subtracts a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Result of the subtraction.
	*/
	Matrix operator -(const nfloat *rhs) const;

	/*!
	Subtracts two Matrix.
	*\param rhs Source matrix.
	*\return Result of the subtraction.
	*/
	Matrix& operator -=(const Matrix &rhs);

	/*!
	Subtracts a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Result of the subtraction.
	*/
	Matrix& operator -=(nfloat rhs);

	/*!
	Subtracts a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Result of the subtraction.
	*/
	Matrix& operator -=(const nfloat *rhs);

	/*!
	Multiplies two Matrix.
	*\param rhs Source matrix.
	*\return Result of the multiplication.
	*/
	Matrix operator *(const Matrix &rhs) const;

	/*!
	Multiplies a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Result of the multiplication.
	*/
	Matrix operator *(nfloat rhs) const;

	/*!
	Multiplies a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Result of the multiplication.
	*/
	Matrix operator *(const nfloat *rhs) const;

	/*!
	Multiplies a array Matrix with a Matrix.
	*\param lhs Source array matrix.
	*\param rhs Source matrix.
	*\return Result of the multiplication.
	*/
	friend Matrix operator *(const nfloat *lhs, const Matrix &rhs);

	/*!
	Multiplies two Matrix.
	*\param rhs Source matrix.
	*\return Result of the multiplication.
	*/
	Matrix& operator *=(const Matrix &rhs);

	/*!
	Multiplies a Matrix with a scalar.
	*\param rhs Matrix scalar.
	*\return Result of the multiplication.
	*/
	Matrix& operator *=(nfloat rhs);

	/*!
	Multiplies a Matrix with a array Matrix.
	*\param rhs Source array matrix.
	*\return Result of the multiplication.
	*/
	Matrix& operator *=(const nfloat *rhs);

	/*!
	Prints a Matrix to the console.
	*\param ostr Stream buffer.
	*\param mat Source vector.
	*\return A stream buffer of the matrix.
	*/
	friend std::ostream& operator <<(std::ostream &ostr, const Matrix &mat);

	/*!
	Creates a view Matrix.
	*\param camera The position of the camera.
	*\param centre The direction that the camera is pointing.
	*\param up The direction that is 'up' from the camera's point of view.
	*\return The created view matrix.
	*/
	NEON_STATIC Matrix createLookAt(const Vector3 &camera, const Vector3 &centre, const Vector3 &up);

	/*!
	Builds an orthogonal projection Matrix.
	*\param width Width of the view volume.
	*\param height Height of the view volume.
	*\param zNearPlane Minimum z-value of the view volume.
	*\param zFarPlane Maximum z-value of the view volume.
	*\return The projection matrix.
	*/
	NEON_STATIC Matrix createOrthographic(const nfloat width, const nfloat height, const nfloat zNearPlane, const nfloat zFarPlane);

	/*!
	Builds a customised, orthogonial projection Matrix.
	*\param leftRight Minimum and Maximum of the X view volume
	*\param bottomTop Minimum and Maximum of the Y view volume.
	*\param zNearPlane Minimum z-value of the view volume.
	*\param zFarPlane Maximum z-value of the view volume.
	*\return The projection matrix.
	*/
	NEON_STATIC Matrix createOrthoGraphicOffCentre(const Vector3 &leftRight, const Vector3 &bottomTop, const nfloat zNearPlane, const nfloat zFarPlane);

	/*!
	Builds a perspective projection Matrix.
	*\param width Width of the view volume at the near view plane.
	*\param height Height of the view volume at the near view plane.
	*\param nearPlaneDistance Distance to the near view plane.
	*\param farPlaneDistance Distance to the far view plane.
	*\return The projection matrix.
	*/
	NEON_STATIC Matrix createPerspective(const nfloat width, const nfloat height, const nfloat nearPlaneDistance, const nfloat farPlaneDistance);

	/*!
	Builds a perspective project Matrix based on a field of view.
	*\param fieldOfView Field of view in radians.
	*\param aspectRatio Aspect Ratio, defined as view space width divided by height.
	*\param nearPlaneDistance Distance to the near view plane.
	*\param farPlaneDistance Distance to the far view plane.
	*\return The perspective projection matrix.
	*/
	NEON_STATIC Matrix createPerspectiveFieldOfView(const nfloat fieldOfView, const nfloat aspectRatio, const nfloat nearPlaneDistance, const nfloat farPlaneDistance);

	/*!
	Returns an x axis rotation Matrix.
	*\param angle Angle in radians.
	*\return The rotation matrix.
	*/
	NEON_STATIC Matrix createRotationX(nfloat angle);

	/*!
	Returns an y axis rotation Matrix.
	*\param angle Angle in radians.
	*\return The rotation matrix.
	*/
	NEON_STATIC Matrix createRotationY(nfloat angle);

	/*!
	Returns an z axis rotation Matrix.
	*\param angle Angle in radians.
	*\return The rotation matrix.
	*/
	NEON_STATIC Matrix createRotationZ(nfloat angle);

	/*!
	Creates a scaling Matrix.
	*\param scales Amount to scale by on the x, y, and z axes.
	*\return The scaling matrix.
	*/
	NEON_STATIC Matrix createScale(const Vector3 &scales);

	/*!
	Creates a scaling Matrix.
	*\param scale Amount to scale by.
	*\return The scaling matrix.
	*/
	NEON_STATIC Matrix createScale(const nfloat scale);

	/*!
	Creates a scaling Matrix.
	*\param x Value to scale by on the x axis.
	*\param y Value to scale by on the y axis.
	*\param z Value to scale by on the z axis.
	*\return The scaling matrix.
	*/
	NEON_STATIC Matrix createScale(const nfloat x, const nfloat y, const nfloat z);

	/*!
	Creates a translation Matrix.
	*\param position Amount to translate by on the x, y, and z axes.
	*\return The translation matrix.
	*/
	NEON_STATIC Matrix createTranslation(const Vector3 &position);

	/*!
	Creates a translation Matrix.
	*\param x Value to translate by on the x axis.
	*\param y Value to translate by on the y axis.
	*\param z Value to translate by on the z axis.
	*\return The translation matrix.
	*/
	NEON_STATIC Matrix createTranslation(const nfloat x, const nfloat y, const nfloat z);

	/*!
	Pointer for direct copy.
	*\return Pointer to the first index of the Matrix.
	*/
	NEON_INLINE const nfloat* data() const { return m.data(); }
	
	/*!
	Pointer for direct copy.
	*\return Pointer to the first index of the Matrix.
	*/
	NEON_INLINE nfloat* data() { return m.data(); }

	/*!
	Calculates the determinant of the Matrix.
	*\return The determinant of the matrix.
	*/
	nfloat determinant() const;

	/*!
	Creates a new Matrix that rotates around an arbitrary vector.
	*\param axis The axis to rotate around.
	*\param angle To rotate around the vector.
	*/
	void fromAxisAngle(const Vector3 &axis, nfloat angle);

	/*!
	Returns a new Identity Matrix.
	*\return The identity matrix.
	*/
	NEON_STATIC Matrix identity;

	/*!
	Calculates the inverse of a Matrix.
	*\param matrix Source matrix.
	*\return the inverse of the matrix.
	*/
	NEON_STATIC Matrix invert(const Matrix &matrix);

	/*!
	Linearly interpolates between the corresponding values of two matrices.
	*\param mat1 Source matrix.
	*\param mat2 Source matrix.
	*\param amount Interpolation value.
	*\return Resulting matrix.
	*/
	NEON_STATIC Matrix lerp(const Matrix &mat1, const Matrix &mat2, const nfloat amount);

	/*!
	Sets the specified index of the Matrix with the specified value.
	*\param index Matrix index.
	*\param value New value.
	*/
	void mat(const nuint8 index, const nfloat value);

	/*!
	Returns the value of the specified index.
	*\return Value of the specified index.
	*/
	nfloat mat(const nuint8 index) const;

	/*!
	Transposes the rows and columns of a Matrix.
	*\param matrix Source matrix.
	*\return Transposed matrix.
	*/
	NEON_STATIC Matrix transpose(const Matrix &matrix);

	/*!
	Transposes the rows and columns of a Matrix.
	*\return Transposed matrix of this matrix.
	*/
	Matrix& transpose();
private:
	boost::array<nfloat, 16> m;
}; // End of class Matrix.

NEON_INLINE Matrix
operator *(const nfloat *lhs, const Matrix &rhs)
{
	return Matrix(
		(lhs[0] * rhs.mat(0)) + (lhs[1] * rhs.mat(4)) + (lhs[2] * rhs.mat(8)) + (lhs[3] * rhs.mat(12)),
		(lhs[0] * rhs.mat(1)) + (lhs[1] * rhs.mat(5)) + (lhs[2] * rhs.mat(9)) + (lhs[3] * rhs.mat(13)),
		(lhs[0] * rhs.mat(2)) + (lhs[1] * rhs.mat(6)) + (lhs[2] * rhs.mat(10)) + (lhs[3] * rhs.mat(14)),
		(lhs[0] * rhs.mat(3)) + (lhs[1] * rhs.mat(7)) + (lhs[2] * rhs.mat(11)) + (lhs[3] * rhs.mat(15)),
		(lhs[4] * rhs.mat(0)) + (lhs[5] * rhs.mat(4)) + (lhs[6] * rhs.mat(8)) + (lhs[7] * rhs.mat(12)),
		(lhs[4] * rhs.mat(1)) + (lhs[5] * rhs.mat(5)) + (lhs[6] * rhs.mat(9)) + (lhs[7] * rhs.mat(13)),
		(lhs[4] * rhs.mat(2)) + (lhs[5] * rhs.mat(6)) + (lhs[6] * rhs.mat(10)) + (lhs[7] * rhs.mat(14)),
		(lhs[4] * rhs.mat(3)) + (lhs[5] * rhs.mat(7)) + (lhs[6] * rhs.mat(11)) + (lhs[7] * rhs.mat(15)),
		(lhs[8] * rhs.mat(0)) + (lhs[9] * rhs.mat(4)) + (lhs[10] * rhs.mat(8)) + (lhs[11] * rhs.mat(12)),
		(lhs[8] * rhs.mat(1)) + (lhs[9] * rhs.mat(5)) + (lhs[10] * rhs.mat(9)) + (lhs[11] * rhs.mat(13)),
		(lhs[8] * rhs.mat(2)) + (lhs[9] * rhs.mat(6)) + (lhs[10] * rhs.mat(10)) + (lhs[11] * rhs.mat(14)),
		(lhs[8] * rhs.mat(3)) + (lhs[9] * rhs.mat(7)) + (lhs[10] * rhs.mat(11)) + (lhs[11] * rhs.mat(15)),
		(lhs[12] * rhs.mat(0)) + (lhs[13] * rhs.mat(4)) + (lhs[14] * rhs.mat(8)) + (lhs[15] * rhs.mat(12)),
		(lhs[12] * rhs.mat(1)) + (lhs[13] * rhs.mat(5)) + (lhs[14] * rhs.mat(9)) + (lhs[15] * rhs.mat(13)),
		(lhs[12] * rhs.mat(2)) + (lhs[13] * rhs.mat(6)) + (lhs[14] * rhs.mat(10)) + (lhs[15] * rhs.mat(14)),
		(lhs[12] * rhs.mat(3)) + (lhs[13] * rhs.mat(7)) + (lhs[14] * rhs.mat(11)) + (lhs[15] * rhs.mat(15))
	);
}

NEON_INLINE std::ostream&
operator <<(std::ostream &ostr, const Matrix &mat)
{
	return ostr << std::fixed << std::setprecision(4)
				<< "Matrix [ M11:" << mat.mat(0) << " M12:" << mat.mat(1) << " M13:" << mat.mat(2) << " M14:" << mat.mat(3) << " ]" << std::endl
				<< "       [ M21:" << mat.mat(4) << " M22:" << mat.mat(5) << " M23:" << mat.mat(6) << " M24:" << mat.mat(7) << " ]" << std::endl
				<< "       [ M31:" << mat.mat(8) << " M32:" << mat.mat(9) << " M33:" << mat.mat(10) << " M34:" << mat.mat(11) << " ]" << std::endl
				<< "       [ M41:" << mat.mat(12) << " M42:" << mat.mat(13) << " M43:" << mat.mat(14) << " M44:" << mat.mat(15) << " ]" << std::endl;
}

} // End of namespace Neon.

#endif // MATRIX_HPP